home *** CD-ROM | disk | FTP | other *** search
Wrap
# Source Generated with Decompyle++ # File: in.pyc (Python 2.5) from copy import copy from datetime import datetime, timedelta from gtcache import gettext as _ from math import ceil from xhtmltools import unescape, xhtmlify from xml.sax.saxutils import unescape from util import checkU, returnsUnicode, checkF, returnsFilename, quoteUnicodeURL, stringify from platformutils import FilenameType import locale import os import os.path as os import urllib import shutil import traceback from download_utils import cleanFilename, nextFreeFilename from feedparser import FeedParserDict from database import DDBObject, defaultDatabase, ObjectNotFoundError from database import DatabaseConstraintError from databasehelper import makeSimpleGetSet from iconcache import IconCache from templatehelper import escape, quoteattr import types import app import template import downloader import config import dialogs import eventloop import feed import filters import menu import prefs import resources import views import random import indexes import util import adscraper import autodler import moviedata import logging import platformutils import filetypes import searchengines import fileutil import imageresize _charset = locale.getpreferredencoding() class Item(DDBObject): '''An item corresponds to a single entry in a feed. It has a single url associated with it. ''' SMALL_ICON_SIZE = (108, 81) BIG_ICON_SIZE = (226, 170) ICON_CACHE_SIZES = [ SMALL_ICON_SIZE, BIG_ICON_SIZE] def __init__(self, entry, linkNumber = 0, feed_id = None, parent_id = None): self.feed_id = feed_id self.parent_id = parent_id self.isContainerItem = None self.isVideo = False self.seen = False self.autoDownloaded = False self.pendingManualDL = False self.downloadedTime = None self.watchedTime = None self.pendingReason = u'' self.entry = entry self.expired = False self.keep = False self.videoFilename = FilenameType('') self.eligibleForAutoDownload = True self.duration = None self.screenshot = None self.resized_screenshots = { } self.resumeTime = 0 self.iconCache = IconCache(self) self.linkNumber = linkNumber self.creationTime = datetime.now() self.updateReleaseDate() self._initRestore() self._lookForFinishedDownloader() DDBObject.__init__(self) self.splitItem() def onRestore(self): if self.iconCache == None: self.iconCache = IconCache(self) else: self.iconCache.dbItem = self self.iconCache.requestUpdate() if not hasattr(self, 'isContainerItem'): self.isContainerItem = None self._initRestore() def _initRestore(self): '''Common code shared between onRestore and __init__.''' self.selected = False self.active = False self.childrenSeen = None self.downloader = None self.expiring = None self.showMoreInfo = False self.updating_movie_info = False def _lookForFinishedDownloader(self): dler = downloader.lookupDownloader(self.getURL()) if dler and dler.isFinished(): self.downloader = dler dler.addItem(self) (getSelected, setSelected) = makeSimpleGetSet(u'selected', changeNeedsSave = False) (getActive, setActive) = makeSimpleGetSet(u'active', changeNeedsSave = False) def getSelectedState(self, view): currentView = app.controller.selection.itemListSelection.currentView if not (self.selected) or view != currentView: return u'normal' elif not self.active: return u'selected-inactive' else: return u'selected' getSelectedState = returnsUnicode(getSelectedState) def toggleShowMoreInfo(self): self.showMoreInfo = not (self.showMoreInfo) self.signalChange(needsSave = False, needsUpdateXML = True) def getMoreInfoState(self): if self.showMoreInfo: return u'more-info' return u'' getMoreInfoState = returnsUnicode(getMoreInfoState) def findChildVideos(self): '''If this item points to a directory, return the set all video files under that directory. ''' videos = set() filename_root = self.getFilename() if os.path.isdir(filename_root): for dirpath, dirnames, filenames in os.walk(filename_root): for name in filenames: filename = os.path.join(dirpath, name) if filetypes.isVideoFilename(filename) or filetypes.isAudioFilename(filename): videos.add(filename) continue return videos def findNewChildren(self): '''If this feed is a container item, walk through its directory and find any new children. Returns True if it found childern and ran signalChange(). ''' filename_root = self.getFilename() if not self.isContainerItem: return False if self.getState() == 'downloading': return False videos = self.findChildVideos() for child in self.getChildren(): videos.discard(child.getFilename()) for video in videos: if not video.startswith(filename_root): raise AssertionError offsetPath = video[len(filename_root):] if offsetPath[0] == '/': offsetPath = offsetPath[1:] FileItem(video, parent_id = self.id, offsetPath = offsetPath) if videos: self.signalChange() return True return False def splitItem(self): '''returns True if it ran signalChange()''' if self.isContainerItem is not None: return self.findNewChildren() if not isinstance(self, FileItem): if self.downloader is None or not self.downloader.isFinished(): return False filename_root = self.getFilename() if os.path.isdir(filename_root): videos = self.findChildVideos() if len(videos) > 1: self.isContainerItem = True for video in videos: if not video.startswith(filename_root): raise AssertionError offsetPath = video[len(filename_root):] if offsetPath[0] == '/': offsetPath = offsetPath[1:] FileItem(video, parent_id = self.id, offsetPath = offsetPath) elif len(videos) == 1: self.isContainerItem = False for video in videos: if not video.startswith(filename_root): raise AssertionError self.videoFilename = video[len(filename_root):] if self.videoFilename[0] in ('/', '\\'): self.videoFilename = self.videoFilename[1:] self.isVideo = True elif not self.getFeedURL().startswith('dtv:directoryfeed'): target_dir = config.get(prefs.NON_VIDEO_DIRECTORY) if not filename_root.startswith(target_dir): if isinstance(self, FileItem): self.migrate(target_dir) else: self.downloader.migrate(target_dir) self.isContainerItem = False else: self.isContainerItem = False self.videoFilename = FilenameType('') self.isVideo = True self.signalChange() return True def removeFromPlaylists(self): itemIDIndex = indexes.playlistsByItemID view = views.playlists.filterWithIndex(itemIDIndex, self.getID()) for playlist in view: playlist.removeItem(self) view = views.playlistFolders.filterWithIndex(itemIDIndex, self.getID()) for playlist in view: playlist.removeItem(self) def updateReleaseDate(self): try: self.releaseDateObj = datetime(*self.getFirstVideoEnclosure().updated_parsed[0:7]) except: try: self.releaseDateObj = datetime(*self.entry.updated_parsed[0:7]) self.releaseDateObj = datetime.min def checkConstraints(self): if self.feed_id is not None: try: obj = self.dd.getObjectByID(self.feed_id) except ObjectNotFoundError: raise DatabaseConstraintError('my feed (%s) is not in database' % self.feed_id) if not isinstance(obj, feed.Feed): msg = 'feed_id points to a %s instance' % obj.__class__ raise DatabaseConstraintError(msg) if self.parent_id is not None: try: obj = self.dd.getObjectByID(self.parent_id) except ObjectNotFoundError: raise DatabaseConstraintError('my parent (%s) is not in database' % self.parent_id) if not isinstance(obj, Item): msg = 'parent_id points to a %s instance' % obj.__class__ raise DatabaseConstraintError(msg) if obj.isContainerItem is not None and not (obj.isContainerItem): msg = 'parent_id is not a containerItem' raise DatabaseConstraintError(msg) if self.parent_id is None and self.feed_id is None: raise DatabaseConstraintError('feed_id and parent_id both None') if self.parent_id is not None and self.feed_id is not None: raise DatabaseConstraintError('feed_id and parent_id both not None') def signalChange(self, needsSave = True, needsUpdateXML = True): self.expiring = None try: del self._state except: pass try: del self._size except: pass if needsUpdateXML: try: del self._itemXML DDBObject.signalChange(self, needsSave = needsSave) def getItemXML(self, viewName): try: xml = self._itemXML except AttributeError: self._calcItemXML() xml = self._itemXML return xml.replace(self._XMLViewName, viewName) def _calcItemXML(self): self._XMLViewName = 'view%dview' % random.randint(9999999, 99999999) self._itemXML = template.fillStaticTemplate('download-item-inner', onlyBody = True, this = self, viewName = self._XMLViewName, templateState = 'unknown') checkU(self._itemXML) def getViewed(self): try: return self._feed.lastViewed >= self.creationTime except: return self.creationTime <= self.getFeed().lastViewed def getFirstVideoEnclosure(self): try: return self._firstVidEnc except: self._calcFirstEnc() return self._firstVidEnc def _calcFirstEnc(self): self._firstVidEnc = getFirstVideoEnclosure(self.entry) def getFirstVideoEnclosureType(self): enclosure = self.getFirstVideoEnclosure() if enclosure and enclosure.has_key('type'): return enclosure['type'] getFirstVideoEnclosureType = returnsUnicode(getFirstVideoEnclosureType) def getURL(self): self.confirmDBThread() videoEnclosure = self.getFirstVideoEnclosure() if videoEnclosure is not None and 'url' in videoEnclosure: return quoteUnicodeURL(videoEnclosure['url'].replace('+', '%20')) else: return u'' getURL = returnsUnicode(getURL) def getQuotedURL(self): return urllib.quote_plus(urllib.unquote(self.getURL().encode('ascii'))).decode('ascii') getQuotedURL = returnsUnicode(getQuotedURL) def hasSharableURL(self): '''Does this item have a URL that the user can share with others? This returns True when the item has a non-file URL. ''' url = self.getURL() if url != u'': pass return not url.startswith(u'file:') def getFeed(self): try: return self._feed except: if self.feed_id is not None: self._feed = self.dd.getObjectByID(self.feed_id) elif self.parent_id is not None: self._feed = self.getParent().getFeed() else: self._feed = None return self._feed def getParent(self): try: return self._parent except: if self.parent_id is not None: self._parent = self.dd.getObjectByID(self.parent_id) else: self._parent = self return self._parent def getFeedURL(self): return self.getFeed().getURL() getFeedURL = returnsUnicode(getFeedURL) def feedExists(self): if self.feed_id: pass return self.dd.idExists(self.feed_id) def getChildren(self): if self.isContainerItem: return views.items.filterWithIndex(indexes.itemsByParent, self.id) else: raise ValueError('%s is not a container item' % self) def setFeed(self, feed_id): self.feed_id = feed_id del self._feed if self.isContainerItem: for item in self.getChildren(): del item._feed item.signalChange() self.signalChange() def executeExpire(self): self.confirmDBThread() self.removeFromPlaylists() UandA = self.getUandA() if not self.isExternal(): self.deleteFiles() self.expired = True if self.isContainerItem: for item in self.getChildren(): item.remove() self.isContainerItem = None self.isVideo = False self.videoFilename = FilenameType('') self.seen = self.keep = self.pendingManualDL = False self.watchedTime = None self.duration = None if self.screenshot: try: os.remove(self.screenshot) self.screenshot = None if self.isExternal(): if self.isDownloaded(): new_item = FileItem(self.getVideoFilename(), feed_id = self.feed_id, parent_id = self.parent_id, deleted = True) if self.downloader is not None: self.downloader.setDeleteFiles(False) self.remove() else: self.signalChange() def expire(self): title = _('Removing %s') % os.path.basename(self.getTitle()) if self.isExternal(): if self.isContainerItem: description = _('Would you like to delete this folder and all of its videos or just remove its entry from the Library?') button = dialogs.BUTTON_DELETE_FILES elif self.isDownloaded(): description = _('Would you like to delete this file or just remove its entry from the Library?') button = dialogs.BUTTON_DELETE_FILE else: self.executeExpire() return None d = dialogs.ThreeChoiceDialog(title, description, dialogs.BUTTON_REMOVE_ENTRY, button, dialogs.BUTTON_CANCEL) def callback(dialog): if not self.idExists(): return None if dialog.choice == button: self.deleteFiles() if dialog.choice in (button, dialogs.BUTTON_REMOVE_ENTRY): self.executeExpire() d.run(callback) elif self.isContainerItem: description = _('This item is a folder. When you remove a folder, any items inside that folder will be deleted.') d = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_DELETE_FILES, dialogs.BUTTON_CANCEL) def callback(dialog): if self.idExists() and dialog.choice == dialogs.BUTTON_DELETE_FILES: self.executeExpire() d.run(callback) else: self.executeExpire() def stopUpload(self): if self.downloader: self.downloader.stopUpload() def startUpload(self): if self.downloader: self.downloader.startUpload() def getString(self, when): '''Get the expiration time a string to display to the user.''' offset = when - datetime.now() if offset.days > 0: result = _('%d days') % offset.days elif offset.seconds > 3600: result = _('%d hours') % ceil(offset.seconds / 3600) else: result = _('%d minutes') % ceil(offset.seconds / 60) return result getString = returnsUnicode(getString) def getExpirationString(self): '''Get the expiration time a string to display to the user.''' expireTime = self.getExpirationTime() if expireTime is None: return u'' else: return _('Expires in %s') % self.getString(expireTime) getExpirationString = returnsUnicode(getExpirationString) def getPausedString(self): '''Get the expiration time a string to display to the user.''' retryTime = None if self.downloader: if self.downloader.getState() == u'offline': retryTime = self.downloader.status['retryTime'] if retryTime is None: return '' else: return _('Will retry in %s') % self.getString(retryTime) else: return _('Paused') else: return u'' getPausedString = returnsUnicode(getPausedString) def getDragType(self): if self.isDownloaded(): return u'downloadeditem' else: return u'item' getDragType = returnsUnicode(getDragType) def getEmblemCSSClass(self): if self.getState() == u'newly-downloaded': return u'newly-downloaded' elif self.getState() == u'new': return u'new' else: return u'' getEmblemCSSClass = returnsUnicode(getEmblemCSSClass) def getEmblemCSSString(self): if self.getState() == u'newly-downloaded': return u'UNWATCHED' elif self.getState() == u'new': return u'NEW' else: return u'' getEmblemCSSString = returnsUnicode(getEmblemCSSString) def getUandA(self): '''Get whether this item is new, or newly-downloaded, or neither.''' state = self.getState() if state == u'new': return (0, 1) elif state == u'newly-downloaded': return (1, 0) else: return (0, 0) def getExpirationTime(self): """Get the time when this item will expire. Returns a datetime object, or None if it doesn't expire. """ self.confirmDBThread() if self.getWatchedTime() is None or not self.isDownloaded(): return None ufeed = self.getFeed() if (ufeed.expire == u'never' or ufeed.expire == u'system') and config.get(prefs.EXPIRE_AFTER_X_DAYS) <= 0: return None elif ufeed.expire == u'feed': expireTime = ufeed.expireTime elif ufeed.expire == u'system': expireTime = timedelta(days = config.get(prefs.EXPIRE_AFTER_X_DAYS)) return self.getWatchedTime() + expireTime def getWatchedTime(self): if not self.getSeen(): return None if self.isContainerItem and self.watchedTime == None: self.watchedTime = datetime.min for item in self.getChildren(): childTime = item.getWatchedTime() if childTime is None: self.watchedTime = None return None if childTime > self.watchedTime: self.watchedTime = childTime continue self.signalChange() return self.watchedTime def getExpiring(self): if self.expiring is None: if not self.getSeen(): self.expiring = False else: ufeed = self.getFeed() if (self.keep and ufeed.expire == u'never' or ufeed.expire == u'system') and config.get(prefs.EXPIRE_AFTER_X_DAYS) <= 0: self.expiring = False else: self.expiring = True return self.expiring def getSeen(self): self.confirmDBThread() if self.isContainerItem: if self.childrenSeen is None: self.childrenSeen = True for item in self.getChildren(): if not item.seen: self.childrenSeen = False break continue return self.childrenSeen else: return self.seen def markItemSeen(self): self.confirmDBThread() if self.seen == False: self.seen = True if self.watchedTime is None: self.watchedTime = datetime.now() self.clearParentsChildrenSeen() self.signalChange() def clearParentsChildrenSeen(self): if self.parent_id: parent = self.getParent() parent.childrenSeen = None parent.signalChange() def markItemUnseen(self): self.confirmDBThread() if self.isContainerItem: self.childrenSeen = False for item in self.getChildren(): item.seen = False item.signalChange() self.signalChange() elif self.seen == False: return None self.seen = False self.watchedTime = None self.clearParentsChildrenSeen() self.signalChange() def getRSSID(self): self.confirmDBThread() return self.entry['id'] getRSSID = returnsUnicode(getRSSID) def removeRSSID(self): self.confirmDBThread() if 'id' in self.entry: del self.entry['id'] self.signalChange() def setAutoDownloaded(self, autodl = True): self.confirmDBThread() if autodl != self.autoDownloaded: self.autoDownloaded = autodl self.signalChange() def setResumeTime(self, position): if not self.idExists(): return None position = int(position) if self.resumeTime != position: self.resumeTime = position self.signalChange() setResumeTime = eventloop.asIdle(setResumeTime) def getPendingReason(self): self.confirmDBThread() return self.pendingReason getPendingReason = returnsUnicode(getPendingReason) def getAutoDownloaded(self): self.confirmDBThread() return self.autoDownloaded def getLinkNumber(self): self.confirmDBThread() return self.linkNumber def download(self, autodl = False): autodler.resumeDownloader() self.confirmDBThread() manualDownloadCount = views.manualDownloads.len() self.expired = self.keep = self.seen = False if not autodl and manualDownloadCount >= config.get(prefs.MAX_MANUAL_DOWNLOADS): self.pendingManualDL = True self.pendingReason = u'queued for download' self.signalChange() return None else: self.setAutoDownloaded(autodl) self.pendingManualDL = False if self.downloader is None: self.downloader = downloader.getDownloader(self) if self.downloader is not None: self.downloader.setChannelName(platformutils.unicodeToFilename(self.getChannelTitle(True))) if self.downloader.isFinished(): self.onDownloadFinished() else: self.downloader.start() self.signalChange() def pause(self): if self.downloader: self.downloader.pause() def resume(self): self.download(self.getAutoDownloaded()) def isPendingManualDownload(self): self.confirmDBThread() return self.pendingManualDL def isEligibleForAutoDownload(self): self.confirmDBThread() if self.getState() not in (u'new', u'not-downloaded'): return False if self.downloader and self.downloader.getState() in (u'failed', u'stopped', u'paused'): return False ufeed = self.getFeed() if ufeed.getEverything: return True return self.eligibleForAutoDownload def isPendingAutoDownload(self): if self.getFeed().isAutoDownloadable(): pass return self.isEligibleForAutoDownload() def isFailedDownload(self): if self.downloader: pass return self.downloader.getState() == u'failed' def getThumbnailURL(self): self.confirmDBThread() videoEnclosure = self.getFirstVideoEnclosure() if videoEnclosure is not None: try: return videoEnclosure['thumbnail']['url'].decode('ascii', 'replace') for enclosure in self.entry.enclosures: try: return enclosure['thumbnail']['url'].decode('ascii', 'replace') continue except KeyError: continue try: return self.entry['thumbnail']['url'].decode('ascii', 'replace') except: None<EXCEPTION MATCH>KeyError return None getThumbnailURL = returnsUnicode(getThumbnailURL) def getThumbnail(self): self.confirmDBThread() if self.showMoreInfo: (width, height) = Item.BIG_ICON_SIZE else: (width, height) = Item.SMALL_ICON_SIZE if self.iconCache.isValid(): path = self.iconCache.getResizedFilename(width, height) return resources.absoluteUrl(path) elif self.screenshot: path = self.getResizedScreenshot(width, height) return resources.absoluteUrl(path) elif self.isContainerItem: return resources.url(u'images/container-icon.png') else: feedThumbnail = self.getFeed().getItemThumbnail(width, height) if feedThumbnail is not None: return feedThumbnail elif self.showMoreInfo: return resources.url(u'images/thumb-more-info.png') else: return resources.url(u'images/thumb.png') getThumbnail = returnsUnicode(getThumbnail) def getTitle(self): try: return self.entry.title except: try: enclosure = self.getFirstVideoEnclosure() return enclosure['url'].decode('ascii', 'replace') return u'' getTitle = returnsUnicode(getTitle) def getQuotedTitle(self): return urllib.quote_plus(self.getTitle().encode('utf8')).decode('ascii', 'replace') getQuotedTitle = returnsUnicode(getQuotedTitle) def getChannelTitle(self, allowSearchFeedTitle = False): implClass = self.getFeed().actualFeed.__class__ if implClass in (feed.RSSFeedImpl, feed.ScraperFeedImpl): return self.getFeed().getTitle() elif implClass == feed.SearchFeedImpl and allowSearchFeedTitle: return searchengines.getLastEngineTitle() else: return u'' getChannelTitle = returnsUnicode(getChannelTitle) def getRawDescription(self): self.confirmDBThread() try: enclosure = self.getFirstVideoEnclosure() return enclosure['text'] except: try: return self.entry.description return u'' getRawDescription = returnsUnicode(getRawDescription) def getDescription(self): rawDescription = self.getRawDescription() try: purifiedDescription = adscraper.purify(rawDescription) return xhtmlify(u'<span>%s</span>' % (unescape(purifiedDescription),), filterFontTags = True) except: try: return xhtmlify(u'<span>%s</span>' % (unescape(rawDescription),)) return u'<span />' getDescription = returnsUnicode(getDescription) def getAd(self): rawDescription = self.getRawDescription() try: rawAd = adscraper.scrape(rawDescription) return xhtmlify(u'<span>%s</span>' % (unescape(rawAd),)) except: return u'<span />' def looksLikeTorrent(self): """Returns true if we think this item is a torrent. (For items that haven't been downloaded this uses the file extension which isn't totally reliable). """ if self.downloader is not None: return self.downloader.getType() == u'bittorrent' else: return self.getURL().endswith(u'.torrent') def getDetails(self): details = [] reldate = self.getReleaseDate() format = self.getFormat() size = self.getSizeForDisplay() link = self.getLink() if self.isContainerItem: children = self.getChildren() details.append(u'<span class="details-count">%s items</span>' % len(children)) if len(reldate) > 0: details.append(u'<span class="details-date">%s</span>' % escape(reldate)) if len(size) > 0: details.append(u'<span class="details-size">%s</span>' % escape(size)) if len(format) > 0: details.append(u'<span class="details-format">%s</span>' % escape(format)) if self.looksLikeTorrent(): details.append(u'<span class="details-torrent">%s</span>' % _('TORRENT')) if len(link) > 0 and link != self.getURL(): details.append(u'<a class="details-link" href="%s">%s</span>' % (quoteattr(link), _('WEB PAGE'))) out = u'<BR>'.join(details) return out getDetails = returnsUnicode(getDetails) def isTransferring(self): if self.downloader: pass return self.downloader.getState() in (u'uploading', u'downloading') def getDownloadDetails(self): status = self.downloader.status details = [ (_('Total Down:'), formatSizeForDetails(status.get('currentSize', 0)))] if status.get('reasonFailed'): details.append((_('Error:'), status['reasonFailed'])) return details def getTorrentDetails(self): status = self.downloader.status return [ (_('Down Rate:'), formatRateForDetails(status.get('rate', 0))), (_('Down Total:'), formatSizeForDetails(status.get('currentSize', 0))), (_('Up Rate:'), formatRateForDetails(status.get('upRate', 0))), (_('Up Total:'), formatSizeForDetails(status.get('uploaded', 0) * 1024 * 1024))] def getItemDetails(self): rv = [] link = self.getLink() if link: rv.append((_('Web page:'), util.makeAnchor(_('permalink'), link))) url = self.getURL() if url and not url.startswith('file:'): rv.append((_('File link:'), util.makeAnchor(_('direct link to file'), url))) rv.append((_('File type:'), self.getFormat())) if self.isDownloaded(): basename = os.path.basename(self.getFilename()) basename = util.clampText(basename, 40) linkEventURL = u'revealItem?item=%d' % self.getID() if self.isContainerItem: label = _('REVEAL LOCAL FOLDER') else: label = _('REVEAL LOCAL FILE') link = util.makeEventURL(label, linkEventURL) rv.append((_('Filename:'), u'%s<BR />%s' % (platformutils.filenameToUnicode(basename), link))) return rv def getTorrentDetailsFinished(self): status = self.downloader.status return [ (_('Down Total'), formatSizeForDetails(status.get('currentSize', 0))), (_('Up Total'), formatSizeForDetails(status.get('uploaded', 0) * 1024 * 1024))] def makeMoreInfoTable(self, title, moreInfoData): lines = [] lines.append(u'<h3>%s</h3>' % title) lines.append(u'<table cellpadding="0" cellspacing="0">') for label, text in moreInfoData: lines.append(u'<tr><td class="label">%s</td><td class="value">%s</td></tr>' % (label, text)) lines.append(u'</table>') return u'\n'.join(lines) def getMoreInfo(self): details = [ self.makeMoreInfoTable(_('Item Details'), self.getItemDetails())] def addTable(label, data): details.append(self.makeMoreInfoTable(label, data)) if self.looksLikeTorrent(): if self.isTransferring(): addTable(_('Torrent Details'), self.getTorrentDetails()) elif self.downloader and self.downloader.isFinished(): addTable(_('Torrent Details <i>stopped</i>'), self.getTorrentDetailsFinished()) elif self.getState() == u'downloading' or not (self.pendingManualDL) or self.isFailedDownload(): addTable(_('Download Details'), self.getDownloadDetails()) return u'\n'.join(details) getMoreInfo = returnsUnicode(getMoreInfo) def deleteFiles(self): self.confirmDBThread() if self.downloader is not None: self.downloader.removeItem(self) self.downloader = None self.signalChange() def getState(self): '''Get the state of this item. The state will be on of the following: * new -- User has never seen this item * not-downloaded -- User has seen the item, but not downloaded it * downloading -- Item is currently downloading * newly-downloaded -- Item has been downoladed, but not played * expiring -- Item has been played and is set to expire * saved -- Item has been played and has been saved * expired -- Item has expired. Uses caching to prevent recalculating state over and over ''' try: return self._state except AttributeError: self._calcState() return self._state def _calcState(self): self.confirmDBThread() if self.downloader is None or self.downloader.getState() in (u'failed', u'stopped'): if self.pendingManualDL: self._state = u'downloading' elif self.expired: self._state = u'expired' elif (self.getViewed() or self.downloader) and self.downloader.getState() in (u'failed', u'stopped'): self._state = u'not-downloaded' else: self._state = u'new' elif self.downloader.getState() in (u'offline', u'paused'): if self.pendingManualDL: self._state = u'downloading' else: self._state = u'paused' elif not self.downloader.isFinished(): self._state = u'downloading' elif not self.getSeen(): self._state = u'newly-downloaded' elif self.getExpiring(): self._state = u'expiring' else: self._state = u'saved' _calcState = returnsUnicode(_calcState) def getChannelCategory(self): """Get the category to use for the channel template. This method is similar to getState(), but has some subtle differences. getState() is used by the download-item template and is usually more useful to determine what's actually happening with an item. getChannelCategory() is used by by the channel template to figure out which heading to put an item under. * downloading and not-downloaded are grouped together as not-downloaded * Newly downloaded and downloading items are always new if their feed hasn't been marked as viewed after the item's pub date. This is so that when a user gets a list of items and starts downloading them, the list doesn't reorder itself. Once they start watching them, then it reorders itself. """ self.confirmDBThread() if self.downloader is None or not self.downloader.isFinished(): if not self.getViewed(): return u'new' if self.expired: return u'expired' else: return u'not-downloaded' elif not self.getSeen(): if not self.getViewed(): return u'new' return u'newly-downloaded' elif self.getExpiring(): return u'expiring' else: return u'saved' getChannelCategory = returnsUnicode(getChannelCategory) def isDownloadable(self): return self.getState() in (u'new', u'not-downloaded', u'expired') def isDownloaded(self): return self.getState() in (u'newly-downloaded', u'expiring', u'saved') def showSaveButton(self): if self.getState() in (u'newly-downloaded', u'expiring'): pass return not (self.keep) def showSaved(self): if self.getState() in (u'saved',) and self.getState() in (u'newly-downloaded', u'expiring'): pass return self.keep def showTrashButton(self): if self.isDownloaded() and self.getFeedURL() == u'dtv:manualFeed': pass return self.getState() not in (u'downloading', u'paused') def getFailureReason(self): self.confirmDBThread() if self.downloader is not None: return self.downloader.getShortReasonFailed() else: return u'' getFailureReason = returnsUnicode(getFailureReason) def getSizeForDisplay(self): return util.formatSizeForUser(self.getSize()) def getSize(self): if not hasattr(self, '_size'): self._size = self._getSize() return self._size def _getSize(self): fname = self.getFilename() if self.isDownloaded(): try: return util.getsize(fname) except OSError: return 0 except: None<EXCEPTION MATCH>OSError None<EXCEPTION MATCH>OSError if self.downloader is not None: return self.downloader.getTotalSize() else: try: return int(self.getFirstVideoEnclosure()['length']) except: return 0 def getCurrentSize(self): if self.downloader is not None: size = self.downloader.getCurrentSize() else: size = 0 return util.formatSizeForUser(size) getCurrentSize = returnsUnicode(getCurrentSize) def downloadProgress(self): progress = 0 self.confirmDBThread() if self.downloader is None: return 0 else: size = self.downloader.getTotalSize() dled = self.downloader.getCurrentSize() if size == 0: return 0 else: return 100 * dled / size def gotContentLength(self): if self.downloader is None: return False else: return self.downloader.getTotalSize() != -1 def downloadProgressWidth(self): fullWidth = 112 progress = self.downloadProgress() / 100 if progress == 0: return 0 return int(progress * fullWidth) def threeDigitPercentDone(self): return u'%03d' % int(self.downloadProgress()) threeDigitPercentDone = returnsUnicode(threeDigitPercentDone) def downloadInProgress(self): if self.downloader is not None: pass return self.downloader.getETA() != 0 def downloadETA(self): if self.downloader is not None: totalSecs = self.downloader.getETA() if totalSecs == -1: return _('downloading...') else: totalSecs = 0 (mins, secs) = divmod(totalSecs, 60) (hours, mins) = divmod(mins, 60) if hours > 0: time = u'%d:%02d:%02d' % (hours, mins, secs) return _('%s remaining') % time else: time = u'%d:%02d' % (mins, secs) return _('%s remaining') % time downloadETA = returnsUnicode(downloadETA) def getStartupActivity(self): if self.pendingManualDL: return self.pendingReason elif self.downloader: return self.downloader.getStartupActivity() else: return _('starting up...') getStartupActivity = returnsUnicode(getStartupActivity) def downloadRate(self): rate = 0 unit = u'KB/s' if self.downloader is not None: rate = self.downloader.getRate() else: rate = 0 rate /= 1024 if rate > 1024: rate /= 1024 unit = u'MB/s' if rate > 1024: rate /= 1024 unit = u'GB/s' return u'%d%s' % (rate, unit) downloadRate = returnsUnicode(downloadRate) def getPubDate(self): return getReleaseDate() getPubDate = returnsUnicode(getPubDate) def getPubDateParsed(self): return self.getReleaseDateObj() def getReleaseDate(self): try: return self.getReleaseDateObj().strftime('%b %d %Y').decode(_charset) except: return u'' getReleaseDate = returnsUnicode(getReleaseDate) def getReleaseDateObj(self): return self.releaseDateObj def getDurationValue(self): secs = 0 if self.duration not in (-1, None): secs = self.duration / 1000 return secs def getDuration(self, emptyIfZero = True): secs = self.getDurationValue() if secs == 0: if emptyIfZero: return u'' else: return 'n/a' return u'%02d:%02d' % (secs / 60, secs % 60) getDuration = returnsUnicode(getDuration) KNOWN_MIME_TYPES = (u'audio', u'video') KNOWN_MIME_SUBTYPES = (u'mov', u'wmv', u'mp4', u'mp3', u'mpg', u'mpeg', u'avi', u'x-flv', u'x-msvideo', u'm4v', u'mkv', u'm2v') MIME_SUBSITUTIONS = { u'QUICKTIME': u'MOV' } def getFormat(self, emptyForUnknown = True): if self.looksLikeTorrent(): return u'.torrent' try: enclosure = self.entry['enclosures'][0] try: extension = enclosure['url'].split('.')[-1].lower().decode('ascii', 'replace') except: extension == u'' if extension.lower() == u'mp3': return u'.mp3' if enclosure.has_key('type') and len(enclosure['type']) > 0: (mtype, subtype) = enclosure['type'].decode('ascii', 'replace').split('/') mtype = mtype.lower() if mtype in self.KNOWN_MIME_TYPES: format = subtype.split(';')[0].upper() if mtype == u'audio': format += u' AUDIO' if format.startswith(u'X-'): format = format[2:] return u'.%s' % self.MIME_SUBSITUTIONS.get(format, format).lower() if extension in self.KNOWN_MIME_SUBTYPES: return u'.%s' % extension except: pass if emptyForUnknown: return u'' else: return u'unknown' getFormat = returnsUnicode(getFormat) def getTags(self): self.confirmDBThread() try: return self.entry.categories.join(u', ') except: return u'' getTags = returnsUnicode(getTags) def getLicence(self): self.confirmDBThread() try: return self.entry.license except: try: return self.getFeed().getLicense() return u'' getLicence = returnsUnicode(getLicence) def getPeople(self): ret = [] self.confirmDBThread() try: for role in self.getFirstVideoEnclosure().roles: for person in self.getFirstVideoEnclosure().roles[role]: ret.append(person) for role in self.entry.roles: for person in self.entry.roles[role]: ret.append(person) except: pass return u', '.join(ret) getPeople = returnsUnicode(getPeople) def getLink(self): self.confirmDBThread() try: return self.entry.link.decode('ascii', 'replace') except: return u'' def getPaymentLink(self): self.confirmDBThread() try: return self.getFirstVideoEnclosure().payment_url.decode('ascii', 'replace') except: try: return self.entry.payment_url.decode('ascii', 'replace') return u'' def getPaymentHTML(self): self.confirmDBThread() try: ret = self.getFirstVideoEnclosure().payment_html except: try: ret = self.entry.payment_html ret = u'' return u'<span>' + unescape(ret) + u'</span>' getPaymentHTML = returnsUnicode(getPaymentHTML) def makeContextMenu(self, templateName, view): c = app.controller if self.isDownloaded(): if templateName in ('playlist', 'playlist-folder'): label = _('Remove From Playlist') else: label = _('Remove From the Library') items = [ (None, (None, None, None), ((lambda : c.playView(view, self.getID())), _('Play'))), ((lambda : c.playView(view, self.getID(), True)), _('Play Just This Video')), (c.addToNewPlaylist, _('Add to new playlist')), (c.removeCurrentItems, label)] if self.getSeen(): items.append((self.markItemUnseen, _('Mark as Unwatched'))) else: items.append((self.markItemSeen, _('Mark as Watched'))) if self.downloader and self.downloader.getState() == 'finished' and self.downloader.getType() == 'bittorrent': items.append((self.startUpload, _('Restart Upload'))) elif self.getState() == 'downloading': items = [ (self.expire, _('Cancel Download')), (self.pause, _('Pause Download'))] else: items = [ (self.download, _('Download'))] return menu.makeMenu(items) def update(self, entry): UandA = self.getUandA() self.confirmDBThread() try: self.entry = entry self.iconCache.requestUpdate() self.updateReleaseDate() self._calcFirstEnc() finally: self.signalChange() def onDownloadFinished(self): '''Called when the download for this item finishes.''' self.confirmDBThread() self.downloadedTime = datetime.now() if not self.splitItem(): self.signalChange() moviedata.movieDataUpdater.requestUpdate(self) for other in views.items: if other.downloader is None and other.getURL() == self.getURL(): other.downloader = self.downloader self.downloader.addItem(other) other.signalChange(needsSave = False) continue app.delegate.notifyDownloadCompleted(self) def getResizedScreenshot(self, width, height): try: return imageresize.getImage(self.resized_screenshots, width, height) except KeyError: return self.screenshot def resizeScreenshot(self): imageresize.removeResizedFiles(self.resized_screenshots) if self.screenshot: self.resized_screenshots = imageresize.multiResizeImage(self.screenshot, self.ICON_CACHE_SIZES) else: self.resized_screenshots = { } def save(self): self.confirmDBThread() if self.keep != True: self.keep = True self.signalChange() def getDownloadedTime(self): if self.downloadedTime is None: return datetime.min else: return self.downloadedTime def getFilename(self): self.confirmDBThread() try: return self.downloader.getFilename() except: return FilenameType('') getFilename = returnsFilename(getFilename) def getVideoFilename(self): self.confirmDBThread() if self.videoFilename: return os.path.join(self.getFilename(), self.videoFilename) else: return self.getFilename() getVideoFilename = returnsFilename(getVideoFilename) def isNonVideoFile(self): if self.isContainerItem != True: pass return not (self.isVideo) def isExternal(self): '''Returns True iff this item was not downloaded from a Democracy channel. ''' if self.feed_id is not None: pass return self.getFeedURL() == 'dtv:manualFeed' def isPlayable(self): '''Returns True iff this item should have a play button.''' if not self.isContainerItem: if self.isDownloaded(): pass return self.getVideoFilename() elif self.isDownloaded(): pass return len(self.getChildren()) > 0 def getRSSEntry(self): self.confirmDBThread() return self.entry def migrateChildren(self, newdir): if self.isContainerItem: for item in self.getChildren(): item.migrate(newdir) def remove(self): if self.downloader is not None: self.downloader.removeItem(self) self.downloader = None if self.iconCache is not None: self.iconCache.remove() self.iconCache = None imageresize.removeResizedFiles(self.resized_screenshots) if self.isContainerItem: for item in self.getChildren(): item.remove() DDBObject.remove(self) def setupLinks(self): """This is called after we restore the database. Since we don't store references between objects, we need a way to reconnect downloaders to the items after the restore. """ if not isinstance(self, FileItem) and self.downloader is None: self.downloader = downloader.getExistingDownloader(self) if self.downloader is not None: self.signalChange(needsSave = False) self.splitItem() if self.isContainerItem is not None and not os.path.exists(self.getFilename()): self.executeExpire() return None if self.screenshot and not os.path.exists(self.screenshot): self.screenshot = None self.signalChange() if self.duration is None or self.screenshot is None: moviedata.movieDataUpdater.requestUpdate(self) def __str__(self): return 'Item - %s' % self.getTitle() def reconnectDownloaders(): reconnected = set() for item in views.items: item.setupLinks() reconnected.add(item.downloader) for downloader in views.remoteDownloads: if downloader not in reconnected: logging.warn('removing orphaned downloader: %s', downloader.url) downloader.remove() continue manualFeed = util.getSingletonDDBObject(views.manualFeed) manualItems = views.items.filterWithIndex(indexes.itemsByFeed, manualFeed.getID()) for item in manualItems: if item.downloader is None and item.__class__ == Item: logging.warn('removing cancelled external torrent: %s', item) item.remove() continue def getEntryForFile(filename): return FeedParserDict({ 'title': platformutils.filenameToUnicode(os.path.basename(filename)), 'enclosures': [ { 'url': resources.url(filename) }] }) def getEntryForURL(url, contentType = None): if contentType is None: contentType = u'video/x-unknown' else: contentType = unicode(contentType) return FeedParserDict({ 'title': url, 'enclosures': [ { 'url': url, 'type': contentType }] }) class FileItem(Item): def __init__(self, filename, feed_id = None, parent_id = None, offsetPath = None, deleted = False): checkF(filename) filename = os.path.abspath(filename) self.filename = filename self.deleted = deleted self.offsetPath = offsetPath self.shortFilename = cleanFilename(os.path.basename(self.filename)) Item.__init__(self, getEntryForFile(filename), feed_id = feed_id, parent_id = parent_id) moviedata.movieDataUpdater.requestUpdate(self) def getState(self): if self.deleted: return u'expired' elif self.getSeen(): return u'saved' else: return u'newly-downloaded' getState = returnsUnicode(getState) def getChannelCategory(self): """Get the category to use for the channel template. This method is similar to getState(), but has some subtle differences. getState() is used by the download-item template and is usually more useful to determine what's actually happening with an item. getChannelCategory() is used by by the channel template to figure out which heading to put an item under. * downloading and not-downloaded are grouped together as not-downloaded * Items are always new if their feed hasn't been marked as viewed after the item's pub date. This is so that when a user gets a list of items and starts downloading them, the list doesn't reorder itself. * Child items match their parents for expiring, where in getState, they always act as not expiring. """ self.confirmDBThread() if self.deleted: return u'expired' elif not self.getSeen(): return u'newly-downloaded' elif self.parent_id and self.getParent().getExpiring(): return u'expiring' else: return u'saved' def getExpiring(self): return False def showSaveButton(self): return False def getViewed(self): return True def isExternal(self): return self.parent_id is None def executeExpire(self): self.confirmDBThread() self.removeFromPlaylists() if self.isContainerItem: for item in self.getChildren(): item.remove() if not os.path.exists(self.filename): self.remove() elif self.feed_id is None: self.deleted = True self.signalChange() else: url = self.getFeedURL() if url.startswith('dtv:manualFeed') or url.startswith('dtv:singleFeed'): self.remove() else: self.deleted = True self.signalChange() def deleteFiles(self): try: if self.getParent(): dler = self.getParent().downloader if dler: dler.stop(False) if os.path.isfile(self.filename): os.remove(self.filename) elif os.path.isdir(self.filename): shutil.rmtree(self.filename) except: logging.warn('WARNING: error deleting files:\n%s', traceback.format_exc()) def getDownloadedTime(self): self.confirmDBThread() try: return datetime.fromtimestamp(os.path.getctime(self.filename)) except: return datetime.min def getFilename(self): try: return self.filename except: return FilenameType('') getFilename = returnsFilename(getFilename) def download(self, autodl = False): self.deleted = False self.signalChange() def updateReleaseDate(self): try: self.releaseDateObj = datetime.fromtimestamp(os.path.getmtime(self.filename)) except: self.releaseDateObj = datetime.min def getReleaseDateObj(self): if self.parent_id: return self.getParent().releaseDateObj else: return self.releaseDateObj def migrate(self, newDir): self.confirmDBThread() if self.parent_id: parent = self.getParent() self.filename = os.path.join(parent.getFilename(), self.offsetPath) return None if self.shortFilename is None: logging.warn("can't migrate download because we don't have a shortFilename!\nfilename was %s", stringify(self.filename)) return None newFilename = os.path.join(newDir, self.shortFilename) if self.filename == newFilename: return None if os.path.exists(self.filename): newFilename = nextFreeFilename(newFilename) def callback(): self.filename = newFilename self.signalChange() fileutil.migrate_file(self.filename, newFilename, callback) elif os.path.exists(newFilename): self.filename = newFilename self.signalChange() self.migrateChildren(newDir) def setupLinks(self): if self.shortFilename is None: if self.parent_id is None: self.shortFilename = cleanFilename(os.path.basename(self.filename)) else: parent_file = self.getParent().getFilename() if self.filename.startswith(parent_file): self.shortFilename = cleanFilename(self.filename[len(parent_file):]) else: logging.warn('%s is not a subdirectory of %s', self.filename, parent_file) self.updateReleaseDate() Item.setupLinks(self) def expireItems(items): if len(items) == 1: return items[0].expire() hasContainers = False hasExternalItems = False for item in items: if item.isContainerItem: hasContainers = True elif item.isExternal(): hasExternalItems = True if hasContainers and hasExternalItems: break continue title = _('Removing %s items') % len(items) if hasExternalItems: description = _('One or more of these videos was not downloaded from a channel. Would you like to delete these items or just remove their entries from the Library?') else: description = u'Are you sure you want to delete all %s videos?' % len(items) if hasContainers: description += u'\n\n' + _('One or more of these items is a folder. When you remove or delete a folder, any items inside that folder will also be removed or deleted.') if hasExternalItems: d = dialogs.ThreeChoiceDialog(title, description, dialogs.BUTTON_REMOVE_ENTRY, dialogs.BUTTON_DELETE_FILES, dialogs.BUTTON_CANCEL) else: d = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_OK, dialogs.BUTTON_CANCEL) def callback(dialog): if dialog.choice == dialogs.BUTTON_DELETE_FILES: for item in items: if item.idExists() and isinstance(item, FileItem): item.deleteFiles() continue if dialog.choice in (dialogs.BUTTON_OK, dialogs.BUTTON_REMOVE_ENTRY, dialogs.BUTTON_DELETE_FILES): for item in items: if item.idExists(): item.executeExpire() continue d.run(callback) def getFirstVideoEnclosure(entry): '''Find the first video enclosure in a feedparser entry. Returns the enclosure, or None if no video enclosure is found. ''' try: enclosures = entry.enclosures except (KeyError, AttributeError): return None for enclosure in enclosures: if filetypes.isVideoEnclosure(enclosure): return enclosure continue return None def formatRateForDetails(bytes): '''Format a download/upload rate for the more-details view.''' sizeFmt = util.formatSizeForUser(bytes, zeroString = u'-') if bytes > 0: return sizeFmt + u'/s' else: return sizeFmt formatRateForDetails = returnsUnicode(formatRateForDetails) def formatSizeForDetails(bytes): '''Format a disk size for the more-details view.''' return util.formatSizeForUser(bytes, zeroString = u'-') formatSizeForDetails = returnsUnicode(formatSizeForDetails)